#!/usr/bin/env ruby
# Based on asciidoctor main ruby script.
# This is only meant to introspect and log which asciidoc files were used.

require 'asciidoctor'
require 'asciidoctor/cli'

MAIN_FILE_REGEX = /^.*\/rules\/(?<id>S\d+)\/(?:(?<lang>\w+)\/)?rule.adoc$/

class MainFileLogger < Asciidoctor::Extensions::Preprocessor
  include Asciidoctor::Logging

  def process document, reader
    # Enable sourcemap to track source location.
    # This is useful to report more accurate errors in other loggers.
    document.sourcemap = true

    main_file = document.normalize_system_path(reader.file, document.reader.dir)

    # This assumes unix-style path separator.
    if nil == main_file.match(MAIN_FILE_REGEX)
      abort("Main file does not follow expected pattern: #{main_file}")
    end

    logger.info("ASCIIDOC LOGGER MAIN FILE:#{main_file}")
    reader
  end
end

class IncludeLogger < Asciidoctor::Extensions::IncludeProcessor
  include Asciidoctor::Logging

  def initialize document
    @config = {} # Defined in parent class; will be updated by the extension registry mechanism.
    @document = document

    # @document.reader.file is not defined yet at this stage.
    # Therefore we cannot compute the main file path and cache it.
    # This cannot be done once in handles? because the object is then frozen.
    # For these reasons, we end up recomputing the rule directory path each time.
  end

  def get_main_file reader
    # See how include_stack is used:
    # https://github.com/asciidoctor/asciidoctor/blob/f3800cc9c92faf8370041b2b27a61124318ed289/lib/asciidoctor/reader.rb#L669
    if reader.include_stack.empty?
      reader.file
    else
      main_frame = reader.include_stack.fetch(0)
      main_frame.fetch(1)
    end
  end

  def handles? target
    include_path = @document.normalize_system_path(target, @document.reader.dir)

    main_file = get_main_file(@document.reader)
    main_file = @document.normalize_system_path(main_file, @document.reader.dir)
    rule_dir = File.dirname(File.dirname(main_file))
    rule_id = File.basename(rule_dir)
    if rule_id == 'rules'
      # This is a language-agnostic rule description.
      rule_dir = File.dirname(main_file)
      rule_id = File.basename(rule_dir)
    end
    git_dir = File.dirname(File.dirname(rule_dir))
    shared_dir = File.join(git_dir, 'shared_content')

    rule_dir = rule_dir + '/' # Don't allow S100 to include things from S1000.
    if !include_path.start_with?(rule_dir) && !include_path.start_with?(shared_dir)
      logger.info("ASCIIDOC LOGGER CROSSREFERENCE:#{rule_id} cross-references #{include_path}")
    end

    logger.info("ASCIIDOC LOGGER INCLUDE:#{include_path}")

    false # Actually, this include processor does nothing.
  end

  # Intentionnaly no process function here.
end

class SourceLogger < Asciidoctor::Extensions::TreeProcessor
  include Asciidoctor::Logging

  def get_source_location block
    loc = block.source_location # Asciidoctor::Reader::Cursor.
    return "#{loc.file}:#{loc.lineno}"
  end

  def get_rule document
    main_file = document.normalize_system_path(document.reader.file, document.reader.dir)
    res = main_file.match(MAIN_FILE_REGEX)
    lang = res[:lang] || 'default'
    "#{res[:id]}/#{lang}"
  end

  def process document
    rule = get_rule(document)
    source_blocks = document.find_by(context: :listing, style: 'source')

    # Collect individually valid blocks per diff-id.
    blocks_per_id = Hash.new { |hash, key| hash[key] = Array.new }

    # Find blocks with only diff-id but no diff-type, or vice-versa, or invalid diff-type.
    source_blocks.each do |block|
      id = block.attr('diff-id', nil)
      type = block.attr('diff-type', nil)

      if !id and !type
        next # Nothing to validate
      end

      loc = get_source_location(block)

      if !id
        logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id is missing in #{loc}")
        next
      end

      if !type
        logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type is missing in #{loc}")
        next
      elsif !['compliant', 'noncompliant'].include?(type)
        logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type '#{type}' is not valid in #{loc}")
        next
      end

      # The block is valid on its own.
      blocks_per_id[id].push(block)
    end

    # Each diff-id should have:
    #  * exactly 1 noncompliant block, and
    #  * 1 or more compliant blocks.
    # Find blocks that break this rule.
    blocks_per_id.each do |id, blocks|
      # Sort to ensure deterministic output.
      blocks.sort_by! { |block| get_source_location(block) }

      locs = blocks.map { |block| get_source_location(block) }.join(', ')

      compliant = blocks.count { |block| block.attr('diff-type') == 'compliant' }
      if compliant == 0
        logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has zero compliant example: #{locs}")
      end

      noncompliant = blocks.count { |block| block.attr('diff-type') == 'noncompliant' }
      if noncompliant != 1
        message = noncompliant == 0 ? "zero noncompliant example" : "too many noncompliant examples"
        logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has #{message}: #{locs}")
      end
    end
  end
end

Asciidoctor::Extensions.register do
  preprocessor MainFileLogger
  include_processor IncludeLogger.new @document
  treeprocessor SourceLogger
end

invoker = Asciidoctor::Cli::Invoker.new ARGV
GC.start
invoker.invoke!
exit invoker.code
